---
title: 'Manage local state'
sidebar_title: '9. Manage local state'
description: Store and query local data in the Apollo cache
---

import { MultiCodeBlock } from 'gatsby-theme-apollo-docs';
import Disclaimer from '../../shared/disclaimer.mdx';
import {
  ExpansionPanel,
} from 'gatsby-theme-apollo-docs/src/components/expansion-panel';

> Time to accomplish: 20 minutes

Like most web apps, our app relies on a combination of remotely fetched data and locally stored data. We can use Apollo Client to manage _both_ types of data, making it a single source of truth for our application's state. We can even interact with both types of data in a single operation. Let's learn how!

<Disclaimer />

## Define a client-side schema

First, let's define a **client-side GraphQL schema** that's specific to our application client. This isn't _required_ for managing local state, but it enables useful developer tooling and helps us reason about our data.

Add the following definition to `src/index.tsx`, before the initialization of `ApolloClient`:

<MultiCodeBlock>

```tsx:title=src/index.tsx
export const typeDefs = gql`
  extend type Query {
    isLoggedIn: Boolean!
    cartItems: [ID!]!
  }
`;
```

</MultiCodeBlock>

Also add `gql` to the list of symbols imported from `@apollo/client`:

```js:title=index.tsx
import {
  ApolloClient,
  NormalizedCacheObject,
  ApolloProvider,
  gql, // highlight-line
} from '@apollo/client';
```

As you might expect, this looks a lot like a definition from our _server's_ schema, with one difference: we **extend** the `Query` type. You can extend a GraphQL type that's defined in another location to add fields to that type.

In this case, we're adding two fields to `Query`:

* `isLoggedIn`, to track whether the user has an active session
* `cartItems`, to track which launches the user has added to their cart

Finally, let's modify the constructor of `ApolloClient` to provide our client-side schema:

<MultiCodeBlock>

```ts:title=src/index.tsx
const client: ApolloClient<NormalizedCacheObject> = new ApolloClient({
  cache,
  uri: 'http://localhost:4000/graphql',
  headers: {
    authorization: localStorage.getItem('token') || '',
  },
  typeDefs, // highlight-line
});
```

</MultiCodeBlock>

Next, we need to define how we store the values of these local fields on the client.

## Initialize reactive variables

Just like on the server, we can populate client-side schema fields with data from any source we want. Apollo Client provides a couple of useful built-in options for this:

* The same in-memory cache where the results from server-side queries are stored
* **Reactive variables**, which can store arbitrary data _outside_ the cache while still updating queries that depend on them

Both of these options work for most use cases. We'll use reactive variables because they're faster to get started with.

Open `src/cache.ts`. Update its `import` statement to include the `makeVar` function:

```js:title=src/cache.ts
import { InMemoryCache, Reference, makeVar } from '@apollo/client';
```

Then, add the following to the bottom of the file:

<MultiCodeBlock>

```ts:title=src/cache.ts
// Initializes to true if localStorage includes a 'token' key,
// false otherwise
export const isLoggedInVar = makeVar<boolean>(!!localStorage.getItem('token'));

// Initializes to an empty array
export const cartItemsVar = makeVar<string[]>([]);
```

</MultiCodeBlock>

Here we define two reactive variables, one for each of our client-side schema fields. The value we provide to each `makeVar` call sets the variable's initial value.

The values of `isLoggedInVar` and `cartItemsVar` are  _functions_:

* If you call a reactive variable function with zero arguments (e.g., `isLoggedInVar()`), it returns the variable's current value.
* If you call the function with _one_ argument (e.g., `isLoggedInVar(false)`), it _replaces_ the variable's current value with the provided value.

### Update login logic

Now that we're representing login status with a reactive variable, we need to _update_ that variable whenever the user logs in.

Let's return to `login.tsx` and import our new variable:

```tsx:title=src/pages/login.tsx
import { isLoggedInVar } from '../cache';
```

Now, let's also update that variable whenever a user logs in. Modify the `onCompleted` callback for the `LOGIN_USER` mutation to set `isLoggedInVar` to `true`:

```tsx:title=src/pages/login.tsx
onCompleted({ login }) {
  if (login) {
    localStorage.setItem('token', login.token as string);
    localStorage.setItem('userId', login.id as string);
    isLoggedInVar(true); // highlight-line
  }
}
```

We now have our client-side schema and our client-side data sources. On the server side, next we would define resolvers to connect the two. On the client side, however, we define **field policies** instead.

## Define field policies

A field policy specifies how a single GraphQL field in the Apollo Client cache is read and written. Most server-side schema fields don't need a field policy, because the default policy does the right thing: it writes query results directly to the cache and returns those results without any modifications.

However, our client-side fields aren't _stored_ in the cache! We need to define field policies to tell Apollo Client how to query those fields.

In `src/cache.ts`, look at the constructor of `InMemoryCache`:

<MultiCodeBlock>

```ts:title=src/cache.ts
export const cache: InMemoryCache = new InMemoryCache({
  typePolicies: {
    Query: {
      fields: {
        launches: {
          // ...field policy definitions...
        }
      }
    }
  }
});
```

</MultiCodeBlock>

You might remember that we've _already_ defined a field policy here, specifically for the `Query.launches` field when we [added pagination support](./queries/#add-pagination-support) to our `GET_LAUNCHES` query.

Let's add field policies for `Query.isLoggedIn` and `Query.cartItems`:

<MultiCodeBlock>

```ts{5-14}:title=src/cache.ts
export const cache: InMemoryCache = new InMemoryCache({
  typePolicies: {
    Query: {
      fields: {
        isLoggedIn: {
          read() {
            return isLoggedInVar();
          }
        },
        cartItems: {
          read() {
            return cartItemsVar();
          }
        },
        launches: {
          // ...field policy definitions...
        }
      }
    }
  }
});
```

</MultiCodeBlock>

Our two field policies each include a single field: a **`read` function**. Apollo Client calls a field's `read` function whenever that field is queried. The query result uses the function's return value as the _field's_ value, regardless of any value in the cache or on your GraphQL server.

Now, whenever we query one of our client-side schema fields, the value of our corresponding reactive variable is returned. Let's write a query to try it!

## Query local fields

You can include client-side fields in any GraphQL query you write. To do so, you add the `@client` directive to every client-side field in your query. This tells Apollo Client _not_ to fetch that field's value from your server.

### Login status

Let's define a query that includes our new `isLoggedIn` field. Add the following definitions to `index.tsx`:

<MultiCodeBlock>

```tsx:title=src/index.tsx
const IS_LOGGED_IN = gql`
  query IsUserLoggedIn {
    isLoggedIn @client
  }
`;

function IsLoggedIn() {
  const { data } = useQuery(IS_LOGGED_IN);
  return data.isLoggedIn ? <Pages /> : <Login />;
}
```

</MultiCodeBlock>

Also add the missing imports highlighted below:

```tsx:title=index.tsx
import {
  ApolloClient,
  NormalizedCacheObject,
  ApolloProvider,
  gql,
  useQuery // highlight-line
} from '@apollo/client';
import Login from './pages/login'; // highlight-line
```

The `IsLoggedIn` component executes the `IS_LOGGED_IN` query and renders different components depending on the result:

* If the user _isn't_ logged in, the component displays our application's login screen.
* Otherwise, the component displays our application's home page.

Because all of this query's fields are local fields, we don't need to worry about displaying any loading state.

Finally, let's update the `ReactDOM.render` call to use our new `IsLoggedIn` component:

```tsx:title=index.tsx
ReactDOM.render(
  <ApolloProvider client={client}>
    <IsLoggedIn />
  </ApolloProvider>,
  document.getElementById('root')
);
```

### Cart items

Next, let's implement a client-side cart for storing the launches that a user wants to book.

Open `src/pages/cart.tsx` and replace its contents with the following:

<ExpansionPanel title="Click to expand">

<MultiCodeBlock>

```tsx:title=src/pages/cart.tsx
import React, { Fragment } from 'react';
import { gql, useQuery } from '@apollo/client';

import { Header, Loading } from '../components';
import { CartItem, BookTrips } from '../containers';
import { RouteComponentProps } from '@reach/router';
import { GetCartItems } from './__generated__/GetCartItems';

export const GET_CART_ITEMS = gql`
  query GetCartItems {
    cartItems @client
  }
`;

interface CartProps extends RouteComponentProps {}

const Cart: React.FC<CartProps> = () => {
  const { data, loading, error } = useQuery<GetCartItems>(
    GET_CART_ITEMS
  );

  if (loading) return <Loading />;
  if (error) return <p>ERROR: {error.message}</p>;

  return (
    <Fragment>
      <Header>My Cart</Header>
      {data?.cartItems.length === 0 ? (
        <p data-testid="empty-message">No items in your cart</p>
      ) : (
        <Fragment>
          {data?.cartItems.map((launchId: any) => (
            <CartItem key={launchId} launchId={launchId} />
          ))}
          <BookTrips cartItems={data?.cartItems || []} />
        </Fragment>
      )}
    </Fragment>
  );
}

export default Cart;
```

</MultiCodeBlock>

</ExpansionPanel>

Once again, we query a client-side field and use that query's result to populate our UI. The `@client` directive is the only thing that differentiates this code from code that queries a remote field.

> Although both of the queries above _only_ query client-side fields, a single query can query both client-side and server-side fields.

## Modify local fields

When we want to modify a server-side schema field, we execute a mutation that's handled by our server's resolvers. Modifying a _local_ field is more straightforward, because we can directly access the field's source data (in this case, a reactive variable).

### Enable logout

A logged-in user needs to be able to log out of our client as well. Our example app can perform a logout entirely locally, because logged-in status is determined by the presence of a `token` key in `localStorage`.

Open `src/containers/logout-button.tsx`. Replace its contents with the following:

<ExpansionPanel title="Click to expand">

<MultiCodeBlock>

```tsx{14-25}:title=src/containers/logout-button.tsx
import React from 'react';
import styled from 'react-emotion';
import { useApolloClient } from '@apollo/client';

import { menuItemClassName } from '../components/menu-item';
import { isLoggedInVar } from '../cache';
import { ReactComponent as ExitIcon } from '../assets/icons/exit.svg';

const LogoutButton = () => {
  const client = useApolloClient();
  return (
    <StyledButton
      data-testid="logout-button"
      onClick={() => {
        // Evict and garbage-collect the cached user object
        client.cache.evict({ fieldName: 'me' });
        client.cache.gc();

        // Remove user details from localStorage
        localStorage.removeItem('token');
        localStorage.removeItem('userId');

        // Set the logged-in status to false
        isLoggedInVar(false);
      }}
    >
      <ExitIcon />
      Logout
    </StyledButton>
  );
}

export default LogoutButton;

const StyledButton = styled('button')(menuItemClassName, {
  background: 'none',
  border: 'none',
  padding: 0,
});
```

</MultiCodeBlock>

</ExpansionPanel>

The important part of this code is the logout button's `onClick` handler. It does the following:

1. It uses the [`evict` and `gc` methods](https://www.apollographql.com/docs/react/caching/garbage-collection/) to purge the `Query.me` field from our in-memory cache. This field includes data that's specific to the logged-in user, all of which should be removed on logout.
2. It clears `localStorage`, where we persist the logged-in user's ID and session token between visits.
3. It sets the value of our `isLoggedInVar` reactive variable to `false`.

When the reactive variable's value changes, that change is automatically broadcast to every query that depends on the variable's value (specifically, the `IS_LOGGED_IN` query we [defined earlier](#login-status)).

Because of this, when a user clicks the logout button, our `isLoggedIn` component updates to display the login screen.

### Enable trip booking

Let's enable our users to book trips in the client. We've waited so long to implement this core feature because it requires interacting with both local data (the user's cart) and remote data. Now we know how to do both!

Open `src/containers/book-trips.tsx`. Replace its contents with the following:

<ExpansionPanel title="Click to expand">

<MultiCodeBlock>

```tsx{25-33,40-41}:title=src/containers/book-trips.tsx
import React from 'react';
import { gql, useMutation } from '@apollo/client';

import Button from '../components/button';
import { cartItemsVar } from '../cache';
import * as GetCartItemsTypes from '../pages/__generated__/GetCartItems';
import * as BookTripsTypes from './__generated__/BookTrips';

export const BOOK_TRIPS = gql`
  mutation BookTrips($launchIds: [ID]!) {
    bookTrips(launchIds: $launchIds) {
      success
      message
      launches {
        id
        isBooked
      }
    }
  }
`;

interface BookTripsProps extends GetCartItemsTypes.GetCartItems {}

const BookTrips: React.FC<BookTripsProps> = ({ cartItems }) => {
  const [bookTrips, { data }] = useMutation<
    BookTripsTypes.BookTrips,
    BookTripsTypes.BookTripsVariables
  >(
    BOOK_TRIPS,
    {
      variables: { launchIds: cartItems },
    }
  );

  return data && data.bookTrips && !data.bookTrips.success
    ? <p data-testid="message">{data.bookTrips.message}</p>
    : (
      <Button
        onClick={async () => {
          await bookTrips();
          cartItemsVar([]);
        }}
        data-testid="book-button"
      >
        Book All
      </Button>
    );
}

export default BookTrips;
```
</MultiCodeBlock>

</ExpansionPanel>

This component executes the `BOOK_TRIPS` mutation when the **Book All** button is clicked. The mutation requires a list of `launchIds`, which it obtains from the user's locally stored cart (passed as a prop).

After the `bookTrips` function returns, we call `cartItemsVar([])` to clear the user's cart because the trips in the cart have been booked.

A user can now book all the trips in their cart, but they can't yet _add_ any trips to their cart! Let's apply that last touch.

### Enable cart and booking modifications

Open `src/containers/action-button.tsx`. Replace its contents with the following:

<ExpansionPanel title="Click to expand">

<MultiCodeBlock>

```tsx:title=src/containers/action-button.tsx
import React from 'react';
import {
  gql,
  useMutation,
  useReactiveVar,
  Reference
} from '@apollo/client';

import { GET_LAUNCH_DETAILS } from '../pages/launch';
import Button from '../components/button';
import { cartItemsVar } from '../cache';
import * as LaunchDetailTypes from '../pages/__generated__/LaunchDetails';

export { GET_LAUNCH_DETAILS };

export const CANCEL_TRIP = gql`
  mutation cancel($launchId: ID!) {
    cancelTrip(launchId: $launchId) {
      success
      message
      launches {
        id
        isBooked
      }
    }
  }
`;

interface ActionButtonProps extends Partial<LaunchDetailTypes.LaunchDetails_launch> {}

const CancelTripButton: React.FC<ActionButtonProps> = ({ id }) => {
  const [mutate, { loading, error }] = useMutation(
    CANCEL_TRIP,
    {
      variables: { launchId: id },
      update(cache, { data: { cancelTrip } }) {
        // Update the user's cached list of trips to remove the trip that
        // was just canceled.
        const launch = cancelTrip.launches[0];
        cache.modify({
          id: cache.identify({
            __typename: 'User',
            id: localStorage.getItem('userId'),
          }),
          fields: {
            trips(existingTrips: Reference[], { readField }) {
              return existingTrips.filter(
                tripRef => readField("id", tripRef) !== launch.id
              );
            }
          }
        });
      }
    }
  );

  if (loading) return <p>Loading...</p>;
  if (error) return <p>An error occurred</p>;

  return (
    <div>
      <Button
        onClick={() => mutate()}
        data-testid={'action-button'}
      >
        Cancel This Trip
      </Button>
    </div>
  );
};

const ToggleTripButton: React.FC<ActionButtonProps> = ({ id }) => {
  const cartItems = useReactiveVar(cartItemsVar);
  const isInCart = id ? cartItems.includes(id) : false;
  return (
    <div>
      <Button
        onClick={() => {
          if (id) {
            cartItemsVar(
              isInCart
                ? cartItems.filter(itemId => itemId !== id)
                : [...cartItems, id]
            );
          }
        }}
        data-testid={'action-button'}
      >
        {isInCart ? 'Remove from Cart' : 'Add to Cart'}
      </Button>
    </div>
  );
}

const ActionButton: React.FC<ActionButtonProps> =
  ({ isBooked, id }) => (
    isBooked ? <CancelTripButton id={id} /> : <ToggleTripButton id={id} />
  );

export default ActionButton;
```

</MultiCodeBlock>

</ExpansionPanel>

This code defines two complex components:

* A `CancelTripButton`, which is displayed only for trips that the user has already booked
* A `ToggleTripButton`, which enables the user to add or remove a trip from their cart

Let's cover each separately.

#### Canceling a trip

The `CancelTripButton` component executes the `CANCEL_TRIP` mutation, which takes a `launchId` as a variable (indicating which previously booked trip to cancel).

In our call to `useMutation`, we include an **`update` function**. This function is called after the mutation completes, enabling us to update the cache to reflect the server-side cancellation.

Our `update` function obtains the canceled trip from the mutation result, which is passed to the function. It then uses the `modify` method of `InMemoryCache` to filter that trip out of the `trips` field of our cached `User` object.

> The `cache.modify` method is a powerful and flexible tool for interacting with cached data. To learn more about it, see [`cache.modify`](https://www.apollographql.com/docs/react/caching/cache-interaction/#cachemodify).

#### Adding and removing cart items

The `ToggleTripButton` component doesn't execute any GraphQL operations, because it can instead interact directly with the `cartItemsVar` reactive variable.

On click, the button adds its associated trip to the cart if it's missing, or removes it if it's present.

## Finish up

Our application is complete! If you haven't yet, start up your server and client and test out all of the functionality we just added, including:

* Logging in and out
* Adding and removing trips from the cart
* Booking trips
* Canceling a booked trip

You can also start up the version of the client in `final/client` to compare it to your version.

---

Congratulations! 🎉 You've completed the Apollo full-stack tutorial. You're ready to dive into each individual part of the Apollo platform. Return to the [documentation homepage](/) for quick links to recommended articles in each documentation set.
